<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>Fan Speed Control</title>
        <!-- <script src="vue.min.js"></script> -->
        <!-- <script src="Chart.min.js"></script> -->
        <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.3/vue.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.5.0/Chart.min.js"></script>
        <style type="text/css">
        body {
           font-family: Helvetica, arial, nimbussansl, liberationsans, freesans, clean, sans-serif, 'Segoe UI Emoji', 'Segoe UI Symbol'; 
        }
        .para {
            width: 30em;
            float: left;
            margin-left: 2em;
            -color: #E0E0E0;
        }
        .data {
            width: 40em;
            float: right;
            margin-right: 2em;
        }
        table.para-t {
            border-collapse: collapse;
            /*font-family: monospace;*/
            font-size: 0.9em;
            margin: 0em;
            margin-top: 0.5em;
            margin-bottom: 1em;
            border: 1px solid black;
            padding: 3px;
            -background-color: #2D2D2D;
        }
        table.para-t tr, table.para-t th, table.para-t td {
            border: 1px solid black;
            margin: 0;
            padding: 3px;
            text-align: right;
            width: 3em;
        }
        table.config tr {
            margin-bottom: 2px;
        }
        table.config td {
            padding-right: 2em;
        }
        table.para-t th.head, table.para-t td.head {
            text-align: center;
        }
        .big-text-box {
            width: 40em;
        }
        .small-text-box {
            width: 3em;
            text-align: right;
        }
        .chart {
            width: 50em;
            height: 20em;
            margin: 0 auto;
        }
        </style>
    </head>
    <body>
        <div id="app">
            <div class="para">
                <div>
                    Upload a rmc_cool.conf <input class="file-input" type="file" @change="on_file_change">
                </div>
                <table class="para-t">
                    <tr>
                        <template v-if="type == 'inlet'">
                            <th class="head" colspan="2" >{{ type.toUpperCase() }} Linear</th>
                        </template>
                        <template v-else>
                            <th class="head" colspan="2" >{{ type.toUpperCase() }} PID</th>
                        </template>
                    </tr>
                    <tr>
                        <th class="head">Parameter</th><th>Value</th>
                    </tr>
                    <tr v-for="k in k_of[type]"> <td>{{ k }}</td><td><input class="small-text-box" type="text" v-model.number="conf[kn(type, k)]" ></td> </tr>
                    <template v-if="type == 'cpu'">
                        <tr> <td>Tcontrol</td><td><input class="small-text-box" type="text" v-model.number="Tcontrol" ></td> </tr>
                        <tr> <td>Tjmax</td><td><input class="small-text-box" type="text" v-model.number="Tjmax" ></td> </tr>
                    </template>
                </table>

                <template v-if="type != 'inlet'">
                    <table class="para-t">
                        <tr>
                            <td></td><td>X0</td><td>X1</td><td>X2</td>
                        </tr>
                        <tr>
                            <td>&le; a</td><td>{{ fmt_num(leb_x0) }}</td><td>{{ fmt_num(leb_x1) }}</td><td>{{ fmt_num(leb_x2) }}</td>
                        </tr>
                        <tr>
                            <td>&gt; a</td><td>{{ fmt_num(gtb_x0) }}</td><td>{{ fmt_num(gtb_x1) }}</td><td>{{ fmt_num(gtb_x2) }}</td>
                        </tr>
                    </table>
                </template>

            </div>

            <div class="data">
                <table class="config">
                    <tr>
                        <td>Cool Method</td>
                        <td>{{ conf.cool_method }} =&gt; {{ cool_method.join(', ') }}</td>
                    </tr>
                    <tr>
                        <td>PID Alg</td>
                        <td>
                            <input type="radio" v-model="pid_alg" v-bind:value="'old'" id="alg_old"/><label for="alg_old">old</label>
                            <input type="radio" v-model="pid_alg" v-bind:value="'new'" id="alg_new"/><label for="alg_new">new(add parameter a)</label>
                        </td>
                    </tr>
                    <tr>
                        <td>Device</td>
                        <td>
                            <input type="radio" v-model="type" v-bind:value="'inlet'" id="type_inlet" @change="on_type_change"/><label for="type_inlet">Inlet</label>
                            <input type="radio" v-model="type" v-bind:value="'cpu'" id="type_cpu" @change="on_type_change"/><label for="type_cpu">CPU</label>
                            <input type="radio" v-model="type" v-bind:value="'dimm'" id="type_dimm" @change="on_type_change" /><label for="type_dimm">DIMM</label>
                            <input type="radio" v-model="type" v-bind:value="'outlet'" id="type_outlet" @change="on_type_change" /><label for="type_outlet">Outlet</label>
                            <input type="radio" v-model="type" v-bind:value="'pch'" id="type_pch" @change="on_type_change" /><label for="type_pch">PCH</label>
                            <input type="radio" v-model="type" v-bind:value="'hdd'" id="type_hdd" @change="on_type_change" /><label for="type_hdd">HDD</label>
                            <input type="radio" v-model="type" v-bind:value="'ssd'" id="type_ssd" @change="on_type_change" /><label for="type_ssd">SSD</label>
                            <input type="radio" v-model="type" v-bind:value="'nvme'" id="type_nvme" @change="on_type_change" /><label for="type_nvme">NVME</label>
                            <input type="radio" v-model="type" v-bind:value="'gpu'" id="type_gpu" @change="on_type_change" /><label for="type_gpu">GPU</label>
                            <input type="radio" v-model="type" v-bind:value="'fpga'" id="type_fpga" @change="on_type_change" /><label for="type_fpga">FPGA</label>
                            <input type="radio" v-model="type" v-bind:value="'raid'" id="type_raid" @change="on_type_change" /><label for="type_raid">RAID</label>
                        </td>
                    </tr>
                    <tr> <td>Temperature</td><td> <input type="text" v-model="t_s" class="big-text-box"/> </td> </tr>
                </table>
                <table class="para-t">
                    <tr>
                        <th v-for="h in head.split('|')">{{ h }}</th>
                    </tr>

                    <tr v-for="r in res">
                        <td v-for='c in r'>
                            {{ c }}
                        </td>
                    </tr>
                </table>
            </div>
        </div>

        <div class="chart">
            <canvas id="chart"></canvas>
        </div>

    </body>
    <script type="text/javascript">
        var data = {
            type: 'inlet',
            Tcontrol: 10,
            Tjmax: 100,
            // head: 'T|Te_new|X0|X1|X2|Te1|Te2|PWM offset|PWM new',
            // t_s: '60 65 70 75 80 85 90 95 100',
            head: 'T|PWM new fan normal|PWM new fan fail',
            // t_s: '20 22 24 26 28 30 32 34 36 38 40 42 44 46 48 50',
            t_s: '20 22 24 26 28 30 32 34 36 38 40 42 44 46 48 50 60 65 70 75 80 85 90 95 100',
            k_of: {
                inlet: ['a1', 'b1', 'c1', 'a2', 'b2', 'c2'],
                cpu: ['PWM', 'Te', 'Toff', 'Kp', 'Ti', 'Td', 'b', 'dt', 'a'],
                dimm: ['Tsv', 'PWM', 'Te', 'Kp', 'Ti', 'Td', 'b', 'dt', 'a'],
            },
            other_types: ['outlet', 'pch', 'hdd', 'ssd', 'nvme', 'gpu', 'fpga', 'raid'],
            conf: null,
            title: 'Fan Duty by Temperature',
            pid_alg: 'new',
        };
        for (var i=0;i<data.other_types.length;i++) {
            data.k_of[data.other_types[i]] = data.k_of.dimm;
        }
        function parse_conf(s) {
            var k_of_int = [
                'cool_debug',
                'cool_interval',
                'node_mix',
                'cool_method',
                ];
            var lines = s.split('\n');
            var conf = {};
            for (var i=0;i<lines.length;i++) {
                a_line = lines[i].trim();
                if (a_line == '' || a_line.startsWith('#') || a_line.indexOf('=') < 0) {
                    continue;
                }
                var kv = a_line.split('=');
                var k = kv[0].trim();
                var v = kv[1].trim();
                if (k_of_int.indexOf(k) > -1 || k.endsWith('_duty') || k.endsWith('_PWM')) {
                    v = parseInt(v);
                } else {
                    v = parseFloat(v);
                }
                conf[k] = v;
            }
            return conf;
        }
        var def_conf = `
cool_method=1
inlet_coeff_a1=0.07
inlet_coeff_b1=-2.8
inlet_coeff_c1=60
inlet_coeff_a2=0.07
inlet_coeff_b2=-2.7
inlet_coeff_c2=60
`;
        data.conf = parse_conf(def_conf);
        var app = new Vue({
            el: '#app',
            data: data,
            computed: {
                leb_x0: function () {
                    var kv = this.kv;
                    return kv.Kp * (1 + (kv.dt / kv.Ti) + (kv.Td / kv.dt));
                },
                leb_x1: function () {
                    var kv = this.kv;
                    return 0 - kv.Kp * (1 + 2 * (kv.Td / kv.dt));
                },
                leb_x2: function () {
                    var kv = this.kv;
                    return kv.Kp * (kv.Td / kv.dt);
                },
                gtb_x0: function () {
                    var kv = this.kv;
                    return kv.Kp * (1 + (kv.Td / kv.dt));
                },
                gtb_x1: function () {
                    var kv = this.kv;
                    return 0 - kv.Kp * (1 + 2 * (kv.Td / kv.dt));
                },
                gtb_x2: function () {
                    var kv = this.kv;
                    return kv.Kp * (kv.Td / kv.dt);
                },
                t_list: function () {
                    return this.t_s.replace(/\s+/g, ' ').trim().split(' ').map(function(i){return parseInt(i);});
                },
                res: function () {
                    var rows;
                    var data_cols;
                    if (this.type == 'inlet') {
                        rows = this.linear(this.t_list);
                        data_cols = [1, 2];
                    } else {
                        if (this.pid_alg == 'old') {
                            rows = this.pid(this.t_list);
                        } else {
                            rows = this.new_pid(this.t_list);
                        }
                        data_cols = [rows[0].length -1];
                    }
                    var self = this;
                    // window.setTimeout(function(){update_chart(chart, self.head.split('|'), 0, data_cols, rows, self.title);});
                    this.$nextTick(function(){update_chart(chart, self.head.split('|'), 0, data_cols, rows, self.title);});
                    return rows;
                },
                Te_f: function () {
                    if (this.type == 'cpu') {
                        return this.Te_cpu;
                    } else {
                        return function (i) {return i - this.kv.Tsv;};
                    }
                },
                kv: function() {
                    // conf key value
                    var o = {};
                    var ks = this.k_of[this.type];
                    for (var k in ks) {
                        o[ks[k]] = this.conf[this.kn(this.type, ks[k])]
                    }
                    return o;
                },
                cool_method: function() {
                    var all_methods = ['Inlet', 'CPU', 'DIMM', 'OUTLET', 'PCH', 'HDD', 'SSD', 'NVME', 'GPU', 'FPGA', 'RAID'];
                    var methods = [];
                    var m = this.conf.cool_method;
                    for (var i=0;i<all_methods.length;i++) {
                        if ((m >> i) & 1) {
                            methods.push(all_methods[i]);
                        }
                    }
                    return methods;
                }
            },
            methods: {
                valid_duty: function(v) {
                    v = Math.ceil(v);
                    v = (v < 0 ? 0 : v);
                    v = (v > 100 ? 100 : v);
                    return v;
                },
                linear: function (t_list) {
                    var self = this;
                    return t_list.map(function (t) {
                        var normal = (self.conf.inlet_coeff_a1) * Math.pow(t, 2) + (self.conf.inlet_coeff_b1) * t + (self.conf.inlet_coeff_c1);
                        normal = self.valid_duty(normal);
                        var fail = (self.conf.inlet_coeff_a2) * Math.pow(t, 2) + (self.conf.inlet_coeff_b2) * t + (self.conf.inlet_coeff_c2);
                        fail = self.valid_duty(fail);
                        return [
                            t,
                            normal,
                            fail
                        ];
                    });
                },
                pid: function (t_list) {
                    var lst = [];
                    var fmt = this.fmt_num;
                    for (var i=0;i<t_list.length;i++) {
                        var t = t_list[i];
                        var x = this.x(t);
                        var t1 = (i == 0 ? t : t_list[i-1]);
                        var t2;
                        if (i == 0) {
                            t2 = t;
                        } else if (i == 1) {
                            t2 = t1;
                        } else {
                            t2 = t_list[i-2];
                        }
                        var pwm_offset = this.PWM_offset(t, t1, t2);
                        var pwm_new = (i == 0 ? this.kv.PWM : lst[i-1][lst[0].length-1]) + pwm_offset;
                        pwm_new = this.valid_duty(pwm_new);
                        lst.push([
                            t,
                            fmt(this.Te_f(t)),
                            fmt(x[0]),
                            fmt(x[1]),
                            fmt(x[2]),
                            fmt(this.Te_f(t1)),
                            fmt(this.Te_f(t2)),
                            pwm_offset,
                            pwm_new
                            ]);
                    }
                    return lst;
                },
                new_pid: function(t_list) {
                    var lst = [];
                    var fmt = this.fmt_num;
                    var t_list2 = [t_list[0], t_list[0]];
                    t_list = t_list2.concat(t_list);
                    console.log(t_list);
                    var kv = this.kv;
                    var j = -1;
                    for (var i=2;i<t_list.length;i++) {
                        j += 1;
                        var t = t_list[i];
                        var t1 = t_list[i-1];
                        var t2 = t_list[i-2];
                        var pwm_offset = this.new_pid_pwm(t, t1, t2, kv.Kp, kv.Ti, kv.Td, kv.dt, kv.a, kv.b);
                        var pwm_new = (j == 0 ? this.kv.PWM : lst[j-1][lst[0].length-1]) + pwm_offset.pwm;
                        pwm_new = this.valid_duty(pwm_new);
                        lst.push([
                            t,
                            fmt(this.Te_f(t)),
                            fmt(pwm_offset.x0),
                            fmt(pwm_offset.x1),
                            fmt(pwm_offset.x2),
                            fmt(this.Te_f(t1)),
                            fmt(this.Te_f(t2)),
                            pwm_offset.pwm,
                            pwm_new
                            ]);
                    }
                    return lst;
                },
                new_pid_pwm: function(t, t1, t2, Kp, Ti, Td, dt, a, b) {
                    var Te_new = this.Te_f(t);
                    var Te1 = this.Te_f(t1);
                    var Te2 = this.Te_f(t2);

                    // # Step 1: calculate the basic X0/X1/X2
                    var X0, X1, X2;
                    var PWM;
                    if (Math.abs(Te_new) <= b) {
                        X0 = (Kp * (1 + (dt / Ti) + (Td / dt)));
                    } else {
                        // # abs(Te_new) > b
                        X0 = (Kp * (1 + (Td / dt)));
                    }
                    X1 = ((-Kp) * (1 + (2 * (Td / dt))));
                    X2 = (Kp * (Td / dt));

                    // # Step 2: check Te/Te1/Te2 condition
                    if (Te_new == Te1 && Te_new == Te2) {
                        X0 = (Kp * (1 + (dt / Ti) + (Td / dt)));
                    }
                    if (Math.abs(Te_new) < a) {
                        PWM = 0; // PWM set as 0, skip step 3 and 4 calculation
                        // return PWM;
                        return { x0: X0, x1: X1, x2: X2, pwm: PWM };
                    }

                    // # Step 3: calculate new X0
                    if (b < a && Math.abs(Te_new) < a) {
                        PWM = 0; // # PWM set as 0, skip step 4 calculation
                        // return PWM;
                        return { x0: X0, x1: X1, x2: X2, pwm: PWM };
                    } else if (b >= a) {
                        if (a <= Math.abs(Te_new) && Math.abs(Te_new) <= b) {
                            X0 = (Kp * (1 + (dt / Ti) + (Td / dt)));
                        }
                        if (Math.abs(Te_new) > b) {
                            X0 = (Kp * (1 + (Td / dt)));
                        }
                        if (Math.abs(Te_new) < a) {
                            // nothing
                        }
                        if (Te_new == Te1 && Te_new == Te2) {
                            X0 = (Kp * (1 + (dt / Ti) + (Td / dt)));
                        }
                    } else {
                        // # b < a and abs(Te_new) >= a
                        // nothing
                    }

                    // # Step 4: Calculate fan duty with carry
                    PWM = Math.ceil((X0 * Te_new) + (X1 * Te1) + (X2 * Te2));
                    return { x0: X0, x1: X1, x2: X2, pwm: PWM };
                },
                Te_cpu: function (t) {
                    return this.Tcontrol + this.kv.Toff - (this.Tjmax - t);
                },
                PWM_offset: function (t, t1, t2) {
                    var x = this.x(t);
                    return Math.ceil(x[0] * this.Te_f(t) + x[1] * this.Te_f(t1) + x[2] * this.Te_f(t2));
                },
                x: function (t) {
                    if (Math.abs(this.Te_f(t)) <= this.kv.a) {
                        // todo: confirm: b => a
                        return [this.leb_x0, this.leb_x1, this.leb_x2];
                    } else {
                        return [this.gtb_x0, this.gtb_x1, this.gtb_x2];
                    }
                },
                on_type_change: function () {
                    if (this.type == 'inlet') {
                        this.head = 'T|PWM new fan normal|PWM new fan fail';
                        // this.t_s = '0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40';
                        // this.t_s = '20 22 24 26 28 30 32 34 36 38 40 42 44 46 48 50';
                    } else {
                        this.head = 'T|Te_new|X0|X1|X2|Te1|Te2|PWM offset|PWM new';
                        // this.t_s = '60 65 70 75 80 85 90 95 100';
                    }
                },
                kn: function(type, k) {
                    // key name in conf
                    if (type == 'inlet') {
                        return 'inlet_coeff_' + k;
                    } else {
                        return type + '_' + k;
                    }
                },
                fmt_num: function(x) {
                    var i = Math.ceil(x);
                    if (i == x) {
                        return i;
                    } else {
                        return x.toFixed(1);
                    }
                }
            }
        });

        function gen_data (head, label_col, data_cols, rows) {
            // label_col: x-axis values
            // data_cols: [head_idx, data1_idx, data2_idx]
            var options = [
                {pointStyle: 'circle', borderColor: "rgba(75,192,192,1)"},
                // {pointStyle: 'rectRot', borderColor: "rgba(249,38,114,1)"},
                {pointStyle: 'rectRot', borderColor: "rgba(253,151,32,1)"},
            ];
            var labels = [];
            for (var i=0;i<rows.length;i++) {
                labels.push(rows[i][label_col]);
            }
            var data = [];
            for (var c=0;c<data_cols.length;c++) {
                var d = [];
                for (var i=0;i<rows.length;i++) {
                    d.push(rows[i][data_cols[c]]);
                }
                data.push(d);
            }
            datasets = [];
            for (var c=0;c<data_cols.length;c++) {
                datasets.push({
                    label: head[data_cols[c]],
                    fill: false,
                    lineTension: 0.1,
                    backgroundColor: "rgba(75,192,192,0.4)",
                    // borderColor: "rgba(75,192,192,1)",
                    borderColor: options[c].borderColor,
                    borderCapStyle: 'butt',
                    borderDash: [],
                    borderDashOffset: 0.0,
                    borderJoinStyle: 'miter',
                    pointStyle: options[c].pointStyle,
                    pointBorderColor: "rgba(75,192,192,1)",
                    pointBackgroundColor: "#fff",
                    pointBorderWidth: 3,
                    pointHoverRadius: 5,
                    pointHoverBackgroundColor: "rgba(75,192,192,1)",
                    pointHoverBorderColor: "rgba(220,220,220,1)",
                    pointHoverBorderWidth: 2,
                    pointRadius: 5,
                    pointHitRadius: 10,
                    data: data[c],
                    spanGaps: false,
                });
            }
            return {
                labels: labels,
                datasets: datasets
            };
        }
        // function gen_data (head, label_col, data_cols, rows)
        function update_chart(chart, head, label_col, data_cols, rows, title) {
            var d = gen_data(head, label_col, data_cols, rows);
            chart.data.labels = d.labels;
            chart.data.datasets = d.datasets;
            chart.options.title.text = title;
            chart.update();
        }
        var ctx = document.getElementById("chart");
        var chart = new Chart(ctx, {
            type: 'line',
            data: {},
            options: {
                title: {
                    display: true,
                    text: 'Custom Chart Title'
                }
            }                    
        });

        function on_file_change(e) {
            var files = e.target.files || e.dataTransfer.files;
            if (!files.length) {
                return;
            }
            var reader = new FileReader();
            reader.onload = (e2) => {
                // data:application/octet-stream;base64,AQABBhEAAOcBBRfDICAgwNggICAgICAgICAgIC…AxMjM0NTY3ODkwwN4xMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTDIICAgICAgICDAwa4=
                // data:text/plain;base64,MTIzNDUNCjIyMzQ1DQozMjM0NQ0K
                var res = e2.target.result;
                var s = atob(res.slice(res.indexOf(',') +1));
                app.conf = parse_conf(s);
            };
            var title = e.target.value;
            title = title.split('\\');
            title = title[title.length-1];
            app.title = title;
            document.title = 'FSC ' + title;
            reader.readAsDataURL(files[0]);
        }
        // todo: download/export conf file
        // function parse_conf(s) {
        //     var k_of_int = [
        //         'cool_debug',
        //         'cool_interval',
        //         'node_mix',
        //         'cool_method',
        //         ];
        //     var sects = [
        //         'inlet',
        //         'cpu',
        //         'dimm',
        //         'outlet',
        //         'pch',
        //         'hdd',
        //         'ssd',
        //         'nvme',
        //         'gpu',
        //         'fpga',
        //         'raid',
        //         ];
        //     var lines = s.split('\n');
        //     var conf = {g: {}};
        //     for (var i=0;i<lines.length;i++) {
        //         a_line = lines[i].trim();
        //         if (a_line == '' || a_line.startsWith('#') || a_line.indexOf('=') < 0) {
        //             continue;
        //         }
        //         var kv = a_line.split('=');
        //         var k = kv[0].trim();
        //         var v = kv[1].trim();
        //         if (k_of_int.indexOf(k) > -1 || k.endsWith('_duty') || k.endsWith('_PWM')) {
        //             v = parseInt(v);
        //         } else {
        //             v = parseFloat(v);
        //         }
        //         var sect = null;
        //         for (var j=0;j<sects.length;j++) {
        //             if (k.search(sects[j]) > -1) {
        //                 sect = sects[j];
        //             }
        //         }
        //         if (sect) {
        //             if (!(sect in conf)) {
        //                 conf[sect] = {};
        //             }
        //             conf[sect][k] = v;
        //         } else {
        //             conf['g'][k] = v;
        //         }
        //     }
        //     return conf;
        // }

    </script>
</html>